﻿using SCG.General;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace SCG.Prism
{
    public partial class PrismConnection : Component
    {
        //Events
        //These events fire in a different thread - GUI clients will need to use the 
        //Dispatcher to marshall the calls to the UI thread
        public event EventHandler<EventArgs> ConnectSuccessful;
        public event EventHandler<MessageEventArgs> ConnectFailed;
        public event EventHandler<EventArgs> Disconnected;
        public event EventHandler<EventArgs> LoginOK;
        public event EventHandler<MessageEventArgs> LoginError;
        public event EventHandler<MessageEventArgs> PrismClientError;
        public event EventHandler<MessageEventArgs> PrismServerError;
        public event EventHandler<PrismUserEventArgs> UserAddedToRoom;
        public event EventHandler<PrismUserEventArgs> UserLeftRoom;
        public event EventHandler<RoomNameEventArgs> JoinedRoom;
        public event EventHandler<EventArgs> StartSignal;
        public event EventHandler<RoomNameEventArgs> RoomRemoved;
        public event EventHandler<RoomNameEventArgs> RoomAdded;
        public event EventHandler<PrismUserMessageEventArgs> ChatMessageReceived;
        public event EventHandler<PrismUserMessageEventArgs> DataMessageReceived;
        public event EventHandler<PrismUserEventArgs> UserInfoChanged;
        public event EventHandler<PrismServerStatsEventArgs> ServerStatsReceived;
        public event EventHandler<MessageEventArgs> AdminMessageReceived;
        public event EventHandler<CustomCommandEventArgs> CustomCommandReceived;
        public event EventHandler<PrismUserEventArgs> UserLatencyUpdated;
        public event EventHandler<RoomCountEventArgs> RoomCountChanged;
        public event EventHandler<RoomInfoEventArgs> RoomListReceived;

        //constructors
        public PrismConnection()
        {
            InitializeComponent();
        }
        public PrismConnection(IContainer container)
        {
            container.Add(this);
            InitializeComponent();
        }

        //a Windows control that will be used to make callbacks thread safe
        public Control ParentControl { get; set; }

        //Attempt to connect!     
        public void Connect()
        {
            if (Connected)
            {
                if (ConnectSuccessful != null)
                    ConnectSuccessful(this, EventArgs.Empty);
                return;
            }

            //validate parameters
            if (Host == null || Host == "")
                throw new InvalidOperationException("Host property must be set before connecting");
            if (Port == 0)
                throw new InvalidOperationException("Port property must be set before connecting");
            if (SubjectName == "")
                throw new InvalidOperationException("SubjectName property must be set before connecting");
            if (ParentControl == null)
                throw new InvalidOperationException("ParentControl must be assigned");

            //create the socket
            _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

#if SILVERLIGHT
            EndPoint ep = new DnsEndPoint(Host, Port);            
#else
            IPHostEntry entry = Dns.GetHostEntry(Host);
            EndPoint ep = null;
            foreach(IPAddress addr in entry.AddressList)
                if (addr.AddressFamily == AddressFamily.InterNetwork)
                {
                    ep = new IPEndPoint(addr, Port);
                    break;
                }
#endif
            //make sure we have an endpoint
            if (ep == null)
                throw new InvalidOperationException("Unable to obtain an Socket EndPoint");            

            //set up asynch event handler
            SocketAsyncEventArgs sae = new SocketAsyncEventArgs();
            sae.RemoteEndPoint = ep;
            sae.Completed += socket_ConnectionCompleted;

            //try to connect to remote host
            _socket.ConnectAsync(sae);
        }

        //Disconnect
        public void Disconnect()
        {
            if (!Connected)
                return;

            //close the socket
            try
            {
                _socket.Close();
            }
            catch
            {
            }

            //We're no longer logged in also
            _loggedIn = false;

            //Trigger Disconnected event to client app
            if (Disconnected != null)
                ParentControl.Invoke(Disconnected, this, EventArgs.Empty);
        }
        private void DisconnectedThreadSafe()
        {
            if (Disconnected != null)
                Disconnected(this, new EventArgs());
        }

        //Are we connected?
        public bool Connected
        {
            get
            {
                if (_socket == null)
                    return false;
                else
                {
                    try
                    {
                        bool isConnected = _socket.Connected;
                        if (_loggedIn && !isConnected)
                        {
                            if (Disconnected != null)
                                Disconnected(this, EventArgs.Empty);
                            _loggedIn = false;
                        }
                        return isConnected;
                    }
                    catch
                    {
                        return false;
                    }
                }
            }
        }

        //Host property - Specifies the host name of the PrismServer being connected to
        public string Host
        {
            get
            {
                return _host;
            }
            set
            {
                //Only allow change to Host property if not Active
                if (!Connected)
                    _host = value;
            }
        }

        //Port property - Specified the port number the component should connect on
        public int Port
        {
            get
            {
                return _port;
            }
            set
            {
                //Only allow change to Port if not activated
                if (!Connected)
                    if (value >= 0)
                        _port = value;
            }
        }

        //SubjectName property - specifies the application context this client is connecting to
        public string SubjectName
        {
            get
            {
                return _subjectName;
            }
            set
            {
                //Only allow change to SubjectName if not active
                if (!Connected)
                    _subjectName = value;
            }
        }                      

        //Access the PrismUser that represents the user who is connected
        public PrismUser ThisUser
        {
            get
            {
                return _user;
            }
            internal set
            {
                _user = value;
            }
        }

        //Is the connection "Logged In" to a valid account?
        public bool LoggedIn
        {
            get
            {
                return _loggedIn;
            }
        }

        //Access the current room name
        public string RoomName
        {
            get
            {
                return _roomName;
            }
        }

        //Access current list of users in the current room
        public List<PrismUser> Users
        {
            get
            {
                return _users;
            }
        }

        //Find a user with the specified name
        public PrismUser FindUser(string userName)
        {
            lock (Users)
            {
                foreach (PrismUser user in Users)
                    if (user.UserName == userName)
                        return user;
            }
            return null;
        }

        //Protocol methods follow

        //Login with existing credentials
        public void Login(string userName, string password)
        {
            WriteTokens( "LOGIN", userName, password, SubjectName );
        }

        //Login as a new user - PrismUser object must be populated
        public void LoginNew(PrismUser newUser)
        {
            WriteTokens("LOGINNEW", newUser.ToString(), SubjectName);
        }

        //Attempt to enter an existing room
        public void EnterRoom(string roomName)
        {
            WriteTokens("JOINROOM", roomName);
        }

        //Attempt to create a new room
        public void CreateRoom(string roomName, int maxUsers)
        {
            WriteTokens("CREATEROOM", roomName, maxUsers);
        }

        //Send a chat message
        public void SendChat(string chatText)
        {
            WriteTokens("CHAT", chatText);
        }

        //Send a data message
        public void SendData(string dataText)
        {
            WriteTokens("DATA", dataText);
        }

        //send a data message to guests in another room
        public void SendData(string roomName, string dataText)
        {
            WriteTokens("DATAROOM", roomName, dataText);
        }

        //Request to save the specified user info changes to the server
        public void SaveUserInfo(PrismUser user)
        {
            WriteTokens("SAVEUSER", user.ToString());
        }

        //Request user info for specific user, server responds with UserInfoChanged event
        public void RequestUserInfo(string userName)        
        {
            WriteTokens("REQUESTUSERINFO", userName);
        }

        //Request Server statistics
        public void ServerStats()
        {
            WriteTokens("SERVERSTATS");
        }

        //Send Custom command to the server
        public void CustomCommand(string CommandName, string Params)
        {
            WriteTokens("CUSTOM", CommandName, Params);
        }

        //Request a list of the rooms with number of guests in each one
        public void RequestRoomList()
        {
            WriteTokens("ROOMLIST");
        }

        //Private members      
        private string _host;
        private int _port;
        private PrismNetworkStream _stream;
        private Socket _socket;
        private Thread _readerThread;
        private string _subjectName = "";
        private PrismUser _user;
        private bool _loggedIn;
        private List<PrismUser> _users = new List<PrismUser>();
        private string _roomName;

        //socket was connected
        private void socket_ConnectionCompleted(object sender, SocketAsyncEventArgs e)
        {
            //See if connection failed
            if (e.SocketError != SocketError.Success)                
            {
                if (ConnectFailed != null)
                    ParentControl.Invoke(ConnectFailed, this, new MessageEventArgs(e.SocketError.ToString()));
                e.Dispose();
                return;
            }

            //Create a new PrismNetworkStream to process this connection
            _stream = new PrismNetworkStream(_socket);

            //Create a new reader thread
            _readerThread = new Thread(ExecuteReaderThread);
            _readerThread.IsBackground = true;
            _readerThread.Start();

            //Signal to client that connect was successful        
            if (ConnectSuccessful != null)
                ParentControl.Invoke(ConnectSuccessful, this, EventArgs.Empty);
            e.Dispose();
        }
        private void ConnectSuccessfulThreadSafe()
        {
            if (ConnectSuccessful != null)
                ConnectSuccessful(this, EventArgs.Empty);
        }
        private void ConnectFailedThreadSafe(string s)
        {
            if (ConnectFailed != null)
                ConnectFailed(this, new MessageEventArgs(s));
        }

        //Write method - write tokens to underlying stream
        private void WriteTokens(params object[] tokens)
        {
            if (Connected)
            {
                //Attempt to read from underlying NetworkStream
                try
                {
                    _stream.WriteTokens(tokens);
                }

                //Disconnect socket on an error
                catch(Exception ex)
                {
                    if (PrismClientError != null)
                        ParentControl.Invoke(PrismClientError, this, new MessageEventArgs(ex.Message));
                    Disconnect();
                }
            }
        }

        //Reader thread execution method
        private void ExecuteReaderThread()
        {
            List<string> tokenList = new List<string>();
            string command;
            string s;
            PrismUser user;

            //Keep thread going as long as PrismConnection is Active
            while (Connected)
            {
                //Read a command from the PrismServer - disconnect on error
                try
                {
                    command = _stream.ReadTokens(tokenList);
                }
                catch(Exception ex)
                {
                    if (PrismClientError != null)
                        ParentControl.Invoke(PrismClientError, this, new MessageEventArgs(ex.Message));
                    Disconnect();
                    break;
                }

                //Process Known PrismProtocol commands
                switch (command)
                {
                    //Return a "PING" to the server - heartbeat keeps connection alive
                    case "PING":
                        WriteTokens("PING");
                        break;

                    //Login Error
                    case "LOGINERROR":
                        //Trigger the LoginError event to client app
                        if (LoginError != null)
                            ParentControl.Invoke(LoginError, this, new MessageEventArgs(tokenList[0]));
                        break;

                    //Login OK message - a UserString object is returned
                    case "LOGINOK":

                        //Create a PrismUser object
                        _user = new PrismUser();

                        //Populate data from User String
                        string Token = tokenList[0];
                        _user.FromString(Token);

                        //Record logged in state locally
                        _loggedIn = true;

                        //Trigger the Login OK event to client app
                        if (LoginOK != null)
                            ParentControl.Invoke(LoginOK, this, EventArgs.Empty);

                        //All defined rooms are also passed - add events for these
                        if (RoomAdded != null)
                        {
                            string roomList = tokenList[1];
                            Tokenizer tokRooms = new Tokenizer(roomList);
                            while (tokRooms.HasTokens)
                            {
                                s = tokRooms.TokenString;
                                ParentControl.Invoke(RoomAdded, this, new RoomNameEventArgs(s));                              
                            }
                        }
                        break;

                    //General Error message
                    case "ERROR":
                        if (PrismServerError != null)
                            ParentControl.Invoke(PrismServerError, this, new MessageEventArgs(tokenList[0]));                            
                        break;

                    //User has joined a room
                    case "JOINROOM":

                        //First delete the existing guest list maintained in current room
                        lock (Users)
                        {
                            Users.Clear();
                        }

                        //Assign room name to local property
                        _roomName = tokenList[0];

                        //Fire an event telling client room name has changed
                        if (JoinedRoom != null)
                            ParentControl.Invoke(JoinedRoom, this, new RoomNameEventArgs(_roomName));

                        //Parse each user object from descriptor strings
                        string userString = tokenList[1];
                        Tokenizer tokUser = new Tokenizer(userString);
                        while (tokUser.HasTokens)
                        {
                            s = tokUser.TokenString;
                            user = new PrismUser();
                            user.FromString(s);

                            //If this is the "current" user, use the same instance
                            if (user.UserName == ThisUser.UserName)
                            {
                                s = user.ToString();
                                ThisUser.FromString(s);
                                user = ThisUser;
                            }
                            lock (Users)
                            {
                                Users.Add(user);
                            }

                            //Fire an event letting client know guest added to current room
                            if (UserAddedToRoom != null)
                                ParentControl.Invoke(UserAddedToRoom, this, new PrismUserEventArgs(user));                                
                        }
                        break;

                    //A new user has entered the current room
                    case "ENTERROOM":

                        //Create a PrismUser object for the user who entered
                        s = tokenList[0];
                        user = new PrismUser();
                        user.FromString(s);

                        //Add the new user to the user list
                        lock (Users)
                        {
                            Users.Add(user);
                        }

                        //Trigger an event
                        if (UserAddedToRoom != null)
                            ParentControl.Invoke(UserAddedToRoom, this, new PrismUserEventArgs(user));                            
                        break;

                    //A user has left the current room
                    case "LEAVEROOM":

                        //Locate the user with the specified user name
                        user = FindUser(tokenList[0]);
                        if (user != null)
                        {
                            //Trigger event to client that user will be removed
                            if (UserLeftRoom != null)
                                ParentControl.Invoke(UserLeftRoom, this, new PrismUserEventArgs(user));                                

                            //Remove them from the room
                            lock (Users)
                            {
                                Users.Remove(user);
                            }
                        }
                        break;

                    //Room count has changed for specified room
                    case "ROOMCOUNTCHANGE":
                        if (RoomCountChanged != null)
                        {
                            string roomName = tokenList[0];
                            int count = Int32.Parse(tokenList[1]);
                            ParentControl.Invoke(RoomCountChanged, this, new RoomCountEventArgs(roomName, count));                            
                        }
                        break;

                    //The subject has Started!  MaxGuests have entered the subject
                    case "START":
                        if (StartSignal != null)
                            ParentControl.Invoke(StartSignal, this, EventArgs.Empty);
                        break;

                    //A room was removed, trigger event to client
                    case "ROOMREMOVED":
                        if (RoomRemoved != null)
                            ParentControl.Invoke(RoomRemoved, this, new RoomNameEventArgs(tokenList[0]));
                        break;

                    //A room was added, notify client of new room name
                    case "ROOMADDED":
                        if (RoomAdded != null)
                            ParentControl.Invoke(RoomAdded, this, new RoomNameEventArgs(tokenList[0]));
                        break;

                    //A chat message has come in
                    case "CHAT":
                        user = FindUser(tokenList[0]);
                        if (user != null)
                            if (ChatMessageReceived != null)
                                ParentControl.Invoke(ChatMessageReceived, this, new PrismUserMessageEventArgs(user, tokenList[1]));                                
                        break;

                    //A data message has come in
                    case "DATA":
                        user = FindUser(tokenList[0]);

                        //it might have come from a user in a different room
                        if (user == null)
                        {
                            user = new PrismUser();
                            user.UserName = tokenList[0];
                        }
                   
                        if (DataMessageReceived != null)
                            ParentControl.Invoke(DataMessageReceived, this, new PrismUserMessageEventArgs(user, tokenList[1]));                                
                        break;

                    //User information for a user has changed
                    case "USERINFOCHANGE":
                        s = tokenList[0];
                        user = new PrismUser();
                        user.FromString(s);

                        //replace password if it's "this" user
                        string savePassword = "";
                        if (user.Password == "")
                            if (user.UserName == ThisUser.UserName)
                                savePassword = ThisUser.Password;

                        //Is there a matching user?
                        PrismUser matchingUser = FindUser(user.UserName);
                        if (matchingUser != null)
                        {
                            //Yes, copy their information
                            s = tokenList[0];
                            matchingUser.FromString(s);
                        }
                        else
                            matchingUser = user;

                        //restore password
                        if (matchingUser.Password == "")
                            matchingUser.Password = savePassword;

                        //And notify client via event
                        if (UserInfoChanged != null)
                            ParentControl.Invoke(UserInfoChanged, this, new PrismUserEventArgs(matchingUser));                            
                        
                        break;

                    //Server stats were received
                    case "SERVERSTATS":
                        if (ServerStatsReceived != null)
                        {
                            string statString = tokenList[0];
                            PrismServerStats stats = new PrismServerStats(statString);
                            ParentControl.Invoke(ServerStatsReceived, this, new PrismServerStatsEventArgs(stats));                            
                        }
                        break;

                        //Room list was received
                    case "ROOMLIST":
                        List<RoomInfo> rooms = new List<RoomInfo>();
                        string[] tokens = tokenList[0].Split('^');
                        int t = 0;
                        while (t < tokens.Length)
                        {
                            if (tokens[t].Trim() == "")
                                break;
                            RoomInfo ri = new RoomInfo();
                            ri.RoomName = tokens[t++];
                            ri.GuestCount = Int32.Parse(tokens[t++]);
                            ri.IsLocked = tokens[t++].ToUpper() == "TRUE";
                            rooms.Add(ri);
                        }
                        if (RoomListReceived != null)
                            ParentControl.Invoke(RoomListReceived, this, new RoomInfoEventArgs(rooms));                            
                        break;

                    //Receieved an Admin Message
                    case "ADMINMSG":
                        if (AdminMessageReceived != null)
                            ParentControl.Invoke(AdminMessageReceived, this, new MessageEventArgs(tokenList[0]));                            
                        break;

                    //A custom command was received
                    case "CUSTOM":
                        if (CustomCommandReceived != null)
                            ParentControl.Invoke(CustomCommandReceived, this, new CustomCommandEventArgs(tokenList[0], tokenList[1]));                            
                        break;

                    //A Latency update was received
                    case "LATENCY":
                        string userName = tokenList[0];
                        int latency = Int32.Parse(tokenList[1]);
                        lock (Users)
                        {
                            foreach (PrismUser u in Users)
                                if (u.UserName == userName)
                                {
                                    u.LatencyMS = latency;
                                    if (UserLatencyUpdated != null)
                                        ParentControl.Invoke(UserLatencyUpdated, this, new PrismUserEventArgs(u));                                        
                                    break;
                                }
                        }
                        break;
                }
                Thread.Sleep(10);
            }
        }

        //thread safe event callers
        private void LoginOKThreadSafe()
        {
            if (LoginOK != null)
                LoginOK(this, new EventArgs());
        }
        private void LoginErrorThreadSafe(string msg)
        {
            if (LoginError != null)
                LoginError(this, new MessageEventArgs(msg));
        }
    }
}
